//
// Copyright (c) 2020 Related Code - http://relatedcode.com
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import MessageKit
import AVFoundation

/// The `PlayerState` indicates the current audio controller state
//-------------------------------------------------------------------------------------------------------------------------------------------------
public enum PlayerState {

	/// The audio controller is currently playing a sound
	case playing

	/// The audio controller is currently in pause state
	case pause

	/// The audio controller is not playing any sound and audioPlayer is nil
	case stopped
}

/// The `MKAudioController` update UI for current audio cell that is playing a sound
/// and also creates and manage an `AVAudioPlayer` states, play, pause and stop.
//-------------------------------------------------------------------------------------------------------------------------------------------------
open class MKAudioController: NSObject, AVAudioPlayerDelegate {

	/// The `AVAudioPlayer` that is playing the sound
	open var audioPlayer: AVAudioPlayer?

	/// The `AudioMessageCell` that is currently playing sound
	open weak var playingCell: AudioMessageCell?

	/// The `MessageType` that is currently playing sound
	open var playingMessage: MessageType?

	/// Specify if current audio controller state: playing, in pause or none
	open private(set) var state: PlayerState = .stopped

	// The `MessagesCollectionView` where the playing cell exist
	public weak var messageCollectionView: MessagesCollectionView?

	/// The `Timer` that update playing progress
	internal var progressTimer: Timer?

	// MARK: - Init Methods
	//---------------------------------------------------------------------------------------------------------------------------------------------
	public init(messageCollectionView: MessagesCollectionView) {
		self.messageCollectionView = messageCollectionView
		super.init()
	}

	/// Used to configure the audio cell UI:
	///	 1. play button selected state;
	///	 2. progresssView progress;
	///	 3. durationLabel text;
	///
	/// - Parameters:
	///   - cell: The `AudioMessageCell` that needs to be configure.
	///   - message: The `MessageType` that configures the cell.
	///
	/// - Note:
	///   This protocol method is called by MessageKit every time an audio cell needs to be configure
	//---------------------------------------------------------------------------------------------------------------------------------------------
	open func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) {

		if playingMessage?.messageId == message.messageId, let collectionView = messageCollectionView, let player = audioPlayer {
			playingCell = cell
			cell.progressView.progress = (player.duration == 0) ? 0 : Float(player.currentTime/player.duration)
			cell.playButton.isSelected = (player.isPlaying == true) ? true : false
			guard let displayDelegate = collectionView.messagesDisplayDelegate else {
				fatalError("MessagesDisplayDelegate has not been set.")
			}
			cell.durationLabel.text = displayDelegate.audioProgressTextFormat(Float(player.currentTime), for: cell, in: collectionView)
		}
	}

	/// Used to start play audio sound
	///
	/// - Parameters:
	///   - message: The `MessageType` that contain the audio item to be played.
	///   - audioCell: The `AudioMessageCell` that needs to be updated while audio is playing.
	//---------------------------------------------------------------------------------------------------------------------------------------------
	open func playSound(for message: MessageType, in audioCell: AudioMessageCell) {

		switch message.kind {
		case .audio(let item):
			playingCell = audioCell
			playingMessage = message
			guard let player = try? AVAudioPlayer(contentsOf: item.url) else {
				print("Failed to create audio player for URL: \(item.url)")
				return
			}
			audioPlayer = player
			audioPlayer?.prepareToPlay()
			audioPlayer?.delegate = self
			audioPlayer?.play()
			state = .playing
			audioCell.playButton.isSelected = true  // show pause button on audio cell
			startProgressTimer()
			audioCell.delegate?.didStartAudio(in: audioCell)
		default:
			print("BasicAudioPlayer failed play sound becasue given message kind is not Audio")
		}
	}

	/// Used to pause the audio sound
	///
	/// - Parameters:
	///   - message: The `MessageType` that contain the audio item to be pause.
	///   - audioCell: The `AudioMessageCell` that needs to be updated by the pause action.
	//---------------------------------------------------------------------------------------------------------------------------------------------
	open func pauseSound(for message: MessageType, in audioCell: AudioMessageCell) {

		audioPlayer?.pause()
		state = .pause
		audioCell.playButton.isSelected = false // show play button on audio cell
		progressTimer?.invalidate()
		if let cell = playingCell {
			cell.delegate?.didPauseAudio(in: cell)
		}
	}

	/// Stops any ongoing audio playing if exists
	//---------------------------------------------------------------------------------------------------------------------------------------------
	open func stopAnyOngoingPlaying() {

		guard let player = audioPlayer, let collectionView = messageCollectionView else { return } // If the audio player is nil then we don't need to go through the stopping logic
		player.stop()
		state = .stopped
		if let cell = playingCell {
			cell.progressView.progress = 0.0
			cell.playButton.isSelected = false
			guard let displayDelegate = collectionView.messagesDisplayDelegate else {
				fatalError("MessagesDisplayDelegate has not been set.")
			}
			cell.durationLabel.text = displayDelegate.audioProgressTextFormat(Float(player.duration), for: cell, in: collectionView)
			cell.delegate?.didStopAudio(in: cell)
		}
		progressTimer?.invalidate()
		progressTimer = nil
		audioPlayer = nil
		playingMessage = nil
		playingCell = nil
	}

	/// Resume a currently pause audio sound
	//---------------------------------------------------------------------------------------------------------------------------------------------
	open func resumeSound() {

		guard let player = audioPlayer, let cell = playingCell else {
			stopAnyOngoingPlaying()
			return
		}
		player.prepareToPlay()
		player.play()
		state = .playing
		startProgressTimer()
		cell.playButton.isSelected = true // show pause button on audio cell
		cell.delegate?.didStartAudio(in: cell)
	}

	/// Toggle audio sound
	//---------------------------------------------------------------------------------------------------------------------------------------------
	open func toggleSound(for message: MessageType, in audioCell: AudioMessageCell) {

		if (state == .playing) || (state == .pause) {
			if (playingMessage?.messageId == message.messageId) {
				if (state == .playing) {
					pauseSound(for: message, in: audioCell)
				} else {
					resumeSound()
				}
			} else {
				stopAnyOngoingPlaying()
				playSound(for: message, in: audioCell)
			}
		} else {
			playSound(for: message, in: audioCell)
		}
	}

	// MARK: - Fire Methods
	//---------------------------------------------------------------------------------------------------------------------------------------------
	@objc private func didFireProgressTimer(_ timer: Timer) {

		guard let player = audioPlayer, let collectionView = messageCollectionView, let cell = playingCell else {
			return
		}
		// check if can update playing cell
		if let playingCellIndexPath = collectionView.indexPath(for: cell) {
			// 1. get the current message that decorates the playing cell
			// 2. check if current message is the same with playing message, if so then update the cell content
			// Note: Those messages differ in the case of cell reuse
			let currentMessage = collectionView.messagesDataSource?.messageForItem(at: playingCellIndexPath, in: collectionView)
			if currentMessage != nil && currentMessage?.messageId == playingMessage?.messageId {
				// messages are the same update cell content
				cell.progressView.progress = (player.duration == 0) ? 0 : Float(player.currentTime/player.duration)
				guard let displayDelegate = collectionView.messagesDisplayDelegate else {
					fatalError("MessagesDisplayDelegate has not been set.")
				}
				cell.durationLabel.text = displayDelegate.audioProgressTextFormat(Float(player.currentTime), for: cell, in: collectionView)
			} else {
				// if the current message is not the same with playing message stop playing sound
				stopAnyOngoingPlaying()
			}
		}
	}

	// MARK: - Private Methods
	//---------------------------------------------------------------------------------------------------------------------------------------------
	private func startProgressTimer() {

		progressTimer?.invalidate()
		progressTimer = nil
		progressTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(MKAudioController.didFireProgressTimer(_:)), userInfo: nil, repeats: true)
	}

	// MARK: - AVAudioPlayerDelegate
	//---------------------------------------------------------------------------------------------------------------------------------------------
	open func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {

		stopAnyOngoingPlaying()
	}

	//---------------------------------------------------------------------------------------------------------------------------------------------
	open func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {

		stopAnyOngoingPlaying()
	}
}
